/**
 * 维护登录状态和账号信息
 */
import { parseQuery, stringifyQuery } from '@gnlab/utils'
import { MemStream } from '@gnlab/utils/rx'
import { Subject, BehaviorSubject, map, filter, firstValueFrom } from 'rxjs'
import config from '@/config'
import { inSCRM, sendToSCRM } from '@/lib/scrm'
import { setToken as setAPIToken, onUnauthorized } from '@/services/common/api'
import { sendNetLog } from '@/services/net-log'
import { confirmUsable as appsConfirmUsable } from './apps'
import {
  AuthMethod,
  LoginStatus,
  getAccount,
  TOKEN_KEY,
  type Account,
  type LoginInfo,
} from './definition'

/**
 * 尝试用外部传入的或持久化存储的 token 进行登录
 */
export function loginByDetectedToken() {
  const query = parseQuery(location.search)
  const token = (query.token ?? '') || (localStorage.getItem(TOKEN_KEY) ?? '')
  if (token) token$$.next({ value: token, mayInvalid: true })
  else token$$.next(null)
}

/**
 * 清除所有能获取到的 token（例如发现 token 失效要求重新登录，要清除残留的 token 记录保证不影响后面的业务行为）
 */
export function clearDetectedToken() {
  const query = parseQuery(location.search)
  if (query.token ?? '') {
    const cleared = { ...query }
    delete cleared['token']
    history.replaceState(undefined, '', '?' + stringifyQuery(cleared))
  }

  localStorage.removeItem(TOKEN_KEY)
}

/**
 * 推送 token 变化
 *
 * mayInvalid:
 * - 为 true 代表明确知道 token 可能无效（例如是持久化的 token 值，可能已过期），如果登录失败，直接抛弃 token，要求重新登录。
 * - 为 false 代表 token 是从安全来源取得，理应有效。那么如果还登录失败（如因为没有使用权限），token 可继续使用。
 */
export const token$$ = new Subject<{ value: string; mayInvalid: boolean } | null>()

// token 持久化存储
token$$.subscribe(token => {
  if (token !== null) localStorage.setItem(TOKEN_KEY, token.value)
  else localStorage.removeItem(TOKEN_KEY)
})

/**
 * 登录信息
 */
const info$$ = new BehaviorSubject<LoginInfo>({ status: LoginStatus.NeedConfirm })
export const info$ = new MemStream(info$$.value, info$$.asObservable())

// - token 取得新值时，获取用户信息、完成登录
// - token 无值时，进入“未登录”状态
token$$.subscribe(async token => {
  if (token === null) return void notLoggedIn()

  const res = await getAccount(token.value)
  if (!res.success) return void (token.mayInvalid ? notLoggedIn(res.error) : loginFailed(res.error))
  const account = res.data
  if (account.method === AuthMethod.apps) {
    const usable = await appsConfirmUsable(account)
    if (!usable) {
      const message = '此服务不可用，请联系管理员开通'
      return void (token.mayInvalid ? notLoggedIn(message) : loginFailed(message))
    }
  }
  info$$.next({ status: LoginStatus.LoggedIn, account })
})

export function notLoggedIn(message?: string) {
  info$$.next({ status: LoginStatus.NotLoggedIn, message })
}
export function loginFailed(message: string) {
  info$$.next({ status: LoginStatus.LoginFailed, message })
}

/**
 * 已登录账号信息（未登录时为 null）
 */
export const account$ = new MemStream(
  null,
  info$$.pipe(map(info => (info.status === LoginStatus.LoggedIn ? info.account : null))),
)

/**
 * 已登录账号的 ID
 */
export const accountId = firstValueFrom(
  account$.pipe(
    filter((account): account is Account => !!account),
    map(account => account.id),
  ),
)

/**
 * 已登录账号是否是管理员（未登录时为 null）
 */
export const isAdmin$ = new MemStream(
  false,
  account$.pipe(map(account => account?.isAdmin ?? null)),
)

/**
 * 当前账号是否要以“客服模式”运行
 */
export const inServiceMode$ = new MemStream(
  !config.SERVICE_MODE ? false : null,
  account$.pipe(
    map(account => {
      if (!config.SERVICE_MODE) return false
      if (!account) return null
      return account.isAdmin ? config.SERVICE_MODE_INCLUDE_ADMIN : true
    }),
  ),
)

// 把管理员状态推送给 SCRM
if (inSCRM) {
  isAdmin$
    .pipe(filter((value): value is boolean => value !== null))
    .subscribe(value => sendToSCRM('is-admin', value))
}

/**
 * 与 API 模块同步登录状态
 */
token$$.subscribe(token => {
  setAPIToken(token?.value ?? '')
})

const unauthorized$$ = new Subject<string | undefined>()
export const unauthorized$ = unauthorized$$.asObservable()
onUnauthorized((message?: string) => {
  void sendNetLog('unauthorized', {
    url: location.href,
    storageToken: localStorage.getItem(TOKEN_KEY),
  })
  unauthorized$$.next(message)

  clearDetectedToken()
  notLoggedIn('登录失效，请重新登录')

  setTimeout(() => window.location.reload(), 1500) // 刷新网页以保证断开原有的 mqtt 订阅、定时任务等内容
})
